package com.piece.core.jpa.support;

import com.piece.core.framework.annotation.query.SqlMultiQuery;
import com.piece.core.framework.support.convert.Convert;
import com.piece.core.db.util.SearchFilter;
import com.piece.core.framework.util.object.BeanUtil;
import com.piece.core.framework.util.object.ClassUtil;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.criteria.*;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.*;

/**
 * Jpa查询工具
 */
public class JpaQuery implements Serializable {

    private EntityManager entityManager;
    /**
     * 要查询的模型对象
     */
    private Class clazz;
    /**
     * 转换结果模型对象
     */
    private Class targetClass;
    /**
     * 查询条件列表
     */
    private Root from;

    private List<Predicate> predicates;

    private CriteriaQuery criteriaQuery;

    private boolean buildCriteriaQuery;

    private CriteriaBuilder criteriaBuilder;

    private Map<String, Predicate> predicates_group;

    private List<Expression> expressions;
    /**
     * 排序方式列表
     */
    private List<Order> orders;
    /**
     * 关联模式：子查询
     */
    private Map<String, JpaQuery> subQuery;
    /**
     * 关联模式：关联查询
     */
    private Map<String, JpaQuery> linkQuery;
    /**
     * 关联查询字段值
     */
    private Object projection;
    /**
     * 分组属性
     */
    private String groupBy;

    private JpaQuery() {
    }

    private JpaQuery(Class clazz, EntityManager entityManager, Class targetClass) {
        this.clazz = clazz;
        this.targetClass = targetClass;
        this.entityManager = entityManager;
        this.criteriaBuilder = entityManager.getCriteriaBuilder();
        this.criteriaQuery = criteriaBuilder.createQuery(targetClass);
        this.from = criteriaQuery.from(clazz);
        this.predicates = new ArrayList<>();
        this.predicates_group = new HashMap<>();
        this.expressions = new ArrayList<>();
        this.orders = new ArrayList<>();
        this.buildCriteriaQuery = false;
    }

    /**
     * 通过类创建查询条件
     *
     * @param clazz         实体类
     * @param entityManager
     * @param targetClass   查询类
     */
    public static JpaQuery forClass(Class clazz, EntityManager entityManager, Class targetClass) {
        if (null == targetClass) {
            targetClass = clazz;
        }
        return new JpaQuery(clazz, entityManager, targetClass);
    }

    public void setRoot(Class clazz) {
        this.from = this.criteriaQuery.from(clazz);
    }

    public void toPredicate(Collection<SearchFilter> filters) {
        for (SearchFilter filter : filters) {
            toPredicate(filter);
        }
    }

    private void toPredicate(SearchFilter filter) {
        String[] names = StringUtils.split(filter.fieldName, ".");
        Path expression = this.from.get(names[0]);
        for (int i = 1; i < names.length; i++) {
            expression = expression.get(names[i]);
        }
        String value = null;
        List valueList = null;
        Iterator iterator = null;
        switch (filter.operator) {
            case EQ:
                addPredicate(filter, this.criteriaBuilder.equal(expression, filter.value));
                break;
            case NEQ:
                addPredicate(filter, this.criteriaBuilder.notEqual(expression, filter.value));
                break;
            case NULL:
                addPredicate(filter, this.criteriaBuilder.isNull(expression));
                break;
            case NNULL:
                addPredicate(filter, this.criteriaBuilder.isNotNull(expression));
                break;
            case EMP:
                addPredicate(filter, this.criteriaBuilder.isEmpty(expression));
                break;
            case NEMP:
                addPredicate(filter, this.criteriaBuilder.isNotEmpty(expression));
                break;
            case LK:
                value = Convert.toStr(filter.value);
                if (value.indexOf("%") < 0) {
                    value = "%" + value + "%";
                }
                addPredicate(filter, this.criteriaBuilder.like(expression, value));
                break;
            case NLK:
                value = Convert.toStr(filter.value);
                if (value.indexOf("%") < 0) {
                    value = "%" + value + "%";
                }
                addPredicate(filter, this.criteriaBuilder.notLike(expression, value));
                break;
            case GT:
                addPredicate(filter, this.criteriaBuilder.greaterThan(expression, (Comparable) filter.value));
                break;
            case LT:
                addPredicate(filter, this.criteriaBuilder.lessThan(expression, (Comparable) filter.value));
                break;
            case GE:
                addPredicate(filter, this.criteriaBuilder.greaterThanOrEqualTo(expression, (Comparable) filter.value));
                break;
            case LE:
                addPredicate(filter, this.criteriaBuilder.lessThanOrEqualTo(expression, (Comparable) filter.value));
                break;
            case BTW:
                valueList = (List) filter.value;
                if (valueList.size() == 2) {
                    addPredicate(filter, this.criteriaBuilder.between(expression, (Comparable) valueList.get(0), (Comparable) valueList.get(1)));
                }
                break;
            case IN:
                valueList = (List) filter.value;
                iterator = valueList.iterator();
                CriteriaBuilder.In in = this.criteriaBuilder.in(expression);
                while (iterator.hasNext()) {
                    in.value(iterator.next());
                }
                addPredicate(filter, in);
                break;
            case NIN:
                valueList = (List) filter.value;
                iterator = valueList.iterator();
                CriteriaBuilder.In not_in = this.criteriaBuilder.in(expression);
                while (iterator.hasNext()) {
                    not_in.value(iterator.next());
                }
                addPredicate(filter, this.criteriaBuilder.not(not_in));
                break;
        }
    }

    private void addPredicate(SearchFilter filter, Predicate predicate) {
        String key = filter.key;
        switch (filter.relation) {
            case AND:
                if ("default".equals(key)) {
                    this.predicates.add(this.criteriaBuilder.and(predicate));
                } else {
                    Predicate predicate_ = this.predicates_group.get(key);
                    if (null == predicate_) {
                        predicate_ = predicate;
                    } else {
                        predicate_ = this.criteriaBuilder.and(predicate_, predicate);
                    }
                    this.predicates_group.put(key, predicate_);
                }
                break;
            case OR:
                if ("default".equals(key)) {
                    this.predicates.add(this.criteriaBuilder.or(predicate));
                } else {
                    Predicate predicate_ = this.predicates_group.get(key);
                    if (null == predicate_) {
                        predicate_ = predicate;
                    } else {
                        predicate_ = this.criteriaBuilder.or(predicate_, predicate);
                    }
                    this.predicates_group.put(key, predicate_);
                }
                break;
        }
    }

    /**
     * 设置查询条件
     */
    public CriteriaQuery criteriaQuery() {
        if (this.buildCriteriaQuery) {
            return this.criteriaQuery;
        }

        if (this.predicates_group.size() > 0) {
            Predicate predicate = null;
            for (Map.Entry entry : this.predicates_group.entrySet()) {
                if (null == predicate) {
                    predicate = this.criteriaBuilder.and((Predicate) entry.getValue());
                } else {
                    predicate = this.criteriaBuilder.or(predicate, (Predicate) entry.getValue());
                }
            }
            this.predicates.add(predicate);
        }

        if (!ClassUtil.isEmpty(this.groupBy)) {
            addMultiSelect();
        }

        if (this.predicates.size() > 0) {
            this.criteriaQuery.where(this.predicates.toArray(new Predicate[this.predicates.size()]));
        }

        addSubCondition(this);
        addLinkCondition(this);

        if (!ClassUtil.isEmpty(this.groupBy)) {
            this.criteriaQuery.groupBy(this.from.get(this.groupBy));
        }
        if (!ClassUtil.isEmpty(this.orders)) {
            this.criteriaQuery.orderBy(this.orders);
        }
        this.buildCriteriaQuery = true;
        return this.criteriaQuery;
    }

    /**
     * 增加子查询
     * TODO: 方法未验证
     */
    private void addSubCondition(JpaQuery query) {
        Map subQuery = query.subQuery;
        if (null == subQuery) {
            return;
        }
        for (Iterator queryIterator = subQuery.keySet().iterator(); queryIterator.hasNext(); ) {
            String key = (String) queryIterator.next();
            JpaQuery sub = (JpaQuery) subQuery.get(key);
            Subquery subquery = this.criteriaQuery.subquery(sub.clazz);
            Root subFrom = subquery.from(sub.clazz);
            subquery.select(subFrom.get(key)).where(this.criteriaBuilder.equal(subFrom.get(key), sub.projection));
            this.criteriaBuilder.and((this.from.get(key)).in(subquery));
        }
    }

    /**
     * 增加关联查询
     * TODO: 方法未验证
     */
    private void addLinkCondition(JpaQuery query) {
        Map linkQuery = query.linkQuery;
        if (null == linkQuery) {
            return;
        }
        List<Selection> selections = new ArrayList<>();
        selections.add(this.criteriaQuery.select(this.from).getSelection());
        JpaQuery subJpaQuery = null;
        Root subFrom = null;

        for (Iterator queryIterator = linkQuery.keySet().iterator(); queryIterator.hasNext(); ) {
            String key = (String) queryIterator.next();
            subJpaQuery = (JpaQuery) linkQuery.get(key);
            Class subKeyType = BeanUtil.getKeyType(subJpaQuery.clazz);
            // 获取子查询Subquery对象
            Subquery subQuery = this.criteriaQuery.subquery(subJpaQuery.clazz);
            // 构建新的子查询字段Mapping对象
            subFrom = subQuery.from(subJpaQuery.clazz);
            Predicate subPredicate = this.criteriaBuilder.equal(subFrom.get(key).as(subKeyType), this.from.get(key).as(subKeyType));
            // 构建where条件
            subQuery.where(subPredicate);
            selections.add(subQuery.select(subFrom).getSelection());
        }
        this.criteriaQuery.multiselect(selections);
    }

    /**
     * 增加子查询
     */
    public void addSubQuery(String propertyName, JpaQuery query) {
        if (null == this.subQuery) {
            this.subQuery = new HashMap();
        }
        if (null == query.projection)
            throw new RuntimeException("子查询字段未设置");

        this.subQuery.put(propertyName, query);
    }

    /**
     * 增加关联查询
     */
    public void addLinkQuery(String propertyName, JpaQuery query) {
        if (null == this.linkQuery) {
            this.linkQuery = new HashMap();
        }
        this.linkQuery.put(propertyName, query);
    }

    private void addMultiSelect() {
        Field[] fields = this.targetClass.getDeclaredFields();
        String column;
        for (Field field : fields) {
            SqlMultiQuery jpaMultiQuery = field.getAnnotation(SqlMultiQuery.class);
            if (null != jpaMultiQuery) {
                column = jpaMultiQuery.attribute();
                if (ClassUtil.isEmpty(column)) {
                    column = field.getName();
                }
                switch (jpaMultiQuery.type()) {
                    case MAX:
                        this.expressions.add(this.criteriaBuilder.max(this.from.get(column)));
                        break;
                    case MIN:
                        this.expressions.add(this.criteriaBuilder.min(this.from.get(column)));
                        break;
                    case SUM:
                        this.expressions.add(this.criteriaBuilder.sum(this.from.get(column)));
                        break;
                    case AVG:
                        this.expressions.add(this.criteriaBuilder.avg(this.from.get(column)));
                        break;
                    case COUNT:
                        this.expressions.add(this.criteriaBuilder.count(this.from));
                        break;
                    case GROUP:
                        this.expressions.add(this.from.get(this.groupBy));
                        break;
                }
            }
        }
        this.criteriaQuery.multiselect(this.expressions);
    }

    /**
     * 添加排序
     */
    public void addOrder(com.piece.core.framework.support.bootstrap.Order order) {
        if (null == this.orders) {
            this.orders = new ArrayList();
        }
        switch (order.getSort()) {
            case ASC:
                this.orders.add(this.criteriaBuilder.asc(this.from.get(order.getAttr())));
                break;
            case DESC:
                this.orders.add(this.criteriaBuilder.desc(this.from.get(order.getAttr())));
                break;
        }
    }

    /**
     * 查询统计数量
     */
    public Long count() {
        Long count = 0L;
        Selection selection = getCriteriaQuery().getSelection();
        this.criteriaQuery.select(this.criteriaBuilder.count(this.from));
        List<Long> list = this.entityManager.createQuery(this.criteriaQuery).getResultList();
        if (list.size() == 1) {
            count = list.get(0);
        } else {
            for (Long length : list) {
                count += length;
            }
        }
        this.criteriaQuery.select(selection);
        return count;
    }

    public Class getModule() {
        return this.clazz;
    }

    public Object getProjection() {
        return this.projection;
    }

    public void setProjection(Object projection) {
        this.projection = projection;
    }

    public Class getClazz() {
        return this.clazz;
    }

    public Class getTargetClass() {
        return this.targetClass;
    }

    public void setTargetClass(Class targetClass) {
        this.targetClass = targetClass;
    }

    public List<Order> getOrders() {
        return this.orders;
    }

    public EntityManager getEntityManager() {
        return this.entityManager;
    }

    public Root getFrom() {
        return this.from;
    }

    public List<Predicate> getPredicates() {
        return this.predicates;
    }

    public List<Expression> getExpressions() {
        return this.expressions;
    }

    public CriteriaQuery getCriteriaQuery() {
        if (!this.buildCriteriaQuery) {
            criteriaQuery();
        }
        return this.criteriaQuery;
    }

    public void setBuildCriteriaQuery(boolean buildCriteriaQuery) {
        this.buildCriteriaQuery = buildCriteriaQuery;
    }

    public CriteriaBuilder getCriteriaBuilder() {
        return this.criteriaBuilder;
    }

    public String getGroupBy() {
        return this.groupBy;
    }

    public void setGroupBy(String groupBy) {
        this.groupBy = groupBy;
    }
}
